info: 本文仅介绍不存在有peer dependency的包时,pnpm是如何组织它的node_modules目录的。对于有peer dependency的更复杂的情况,请参考peer是如何被解析的

pnpm的node_modules使用了符号链接来创建依赖关系的嵌套结构。

node_modules中每个package的每个文件都是指向内容可寻址存储(content-addressable store)的硬链接。假设您安装了依赖于bar@1.0.0foo@1.0.0。 pnpm会将两个package都硬链接到node_modules,如下所示:

node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>/bar
    │           ├── index.js
    │           └── package.json
    └── foo@1.0.0
        └── node_modules
            └── foo -> <store>/foo
                ├── index.js
                └── package.json

这些是node_modules中唯一的“真实”文件。在所有package都被硬链接到node_modules之后,pnpm会创建符号链接来构建嵌套的依赖图结构。

您可能已经注意到,这两个软件包都被硬链接到node_modules文件夹(foo@1.0.0/node_modules/foo)内的子文件夹中。这是为了:

  1. . 允许package导入自己。 foo应该能够require ('foo/package.json'),或者import * as package from "foo/package.json"
  2. 避免循环符号链接。package的直接依赖与间接依赖都位于同一文件夹下(原文为Dependencies of packages are placed in the same folder in which the dependent packages are.)。对于Node.js,依赖关系是在包的node_modules内部还是在父目录中的任何其他node_modules中,都没有关系。

安装的下一阶段是创建依赖的符号链接。bar将被链接到foo@1.0.0/node_modules文件夹:

node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>/bar
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>/foo
            └── bar -> ../../bar@1.0.0/node_modules/bar

接下来,处理直接依赖关系。foo将被符号链接到根node_modules文件夹中,因为foo是项目的依赖项:

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>/bar
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>/foo
            └── bar -> ../../bar@1.0.0/node_modules/bar

这是一个非常简单的例子。但是,无论依赖项的数量和依赖关系图的深度如何,node_modules目录都将保持此结构。

让我们添加qar@2.0.0作为barfoo的依赖项。这是新的目录结构:

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>/foo
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>/qar

如您所见,即使现在依赖关系图变得更深(foo> bar> qar),文件系统中的目录深度仍然相同。

乍一看,这种布局看起来很奇怪,但它与Node的模块解析算法完全兼容!在解析模块时,Node会忽略符号链接,因此当从foo@1.0.0/node_modules/foo/index.js引入bar时,Node不会引入foo@1.0.0/node_modules/bar,而是解析到bar其实际位置(bar@1.0.0/node_modules/bar)。因此,bar也可以找到它位于bar@1.0.0/node_modules中的依赖。

这种目录结构的一大好处是,你只可以访问你的直接依赖。使用展平的node_modules结构会导致你的可以直接访问你的间接依赖。要详细了解为什么这是一个优势,请参阅「pnpm的严格性有助于避免愚蠢的错误